package com.onadake.dao;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import com.onadake.domain.Data;
import com.onadake.domain.DataFilterColumns;
import com.onadake.domain.Entity;
import com.onadake.domain.EntityType;
import com.onadake.domain.Relation;
import com.onadake.domain.RelationFilterColumns;
import com.onadake.domain.RelationType;
import com.onadake.domain.query.Criterium;
import com.onadake.domain.query.Filter;
import com.onadake.domain.query.Order;
import com.onadake.utils.Dates;
import com.onadake.utils.Dates.Month;
import com.onadake.utils.ValidationUtils;

/**
 * Implementation of entity dao which is using my batis framework.
 * 
 * @author radek.hecl
 *
 */
public class MyBatisEntityDao implements EntityDao {

    /**
     * Builder object.
     */
    public static class Builder {

        /**
         * Factory for sql sessions.
         */
        private SqlSessionFactory sqlSessionFactory;

        /**
         * Sets factory for sql sessions.
         * 
         * @param sqlSessionFactory factory for sql sessions
         * @return this instance
         */
        public Builder setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
            this.sqlSessionFactory = sqlSessionFactory;
            return this;
        }

        /**
         * Builds the result object.
         * 
         * @return created object
         */
        public MyBatisEntityDao build() {
            return new MyBatisEntityDao(this);
        }
    }

    /**
     * Factory for sql sessions.
     */
    private SqlSessionFactory sqlSessionFactory;

    /**
     * Creates new instance.
     * 
     * @param builder builder object
     */
    public MyBatisEntityDao(Builder builder) {
        sqlSessionFactory = builder.sqlSessionFactory;
        guardInvariants();
    }

    /**
     * Guards this object to be consistent. Throws exception if this is not the case.
     */
    private void guardInvariants() {
        ValidationUtils.guardNotNull(sqlSessionFactory, "sqlSessionFactory cannot be null");
    }

    @Override
    public void insertEntity(Entity entity) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.insertCore(entity.getCode(), entity.getName(), entity.getUtcCreatedTimestamp(), entity.getType());
            if (!entity.getProfile().isEmpty()) {
                mapper.insertProfile(entity.getCode(), entity.getProfile());
            }
            if (!entity.getJoins().isEmpty()) {
                Map<String, Map<String, Object>> sortedJoins = entity.getSortedJoins();
                for (String table : sortedJoins.keySet()) {
                    mapper.insertJoin(table, entity.getCode(), sortedJoins.get(table));
                }
            }
        } finally {
            session.close();
        }
    }

    @Override
    public void deleteEntity(String code) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.deleteEntity(code);
        } finally {
            session.close();
        }
    }

    @Override
    public List<Entity> selectEntities(Set<String> joinsData, Filter<String> filter) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            // fetch cores with joins
            List<Map<String, Object>> cores = mapper.selectCores(transformJoinLabels(joinsData), transformFilter(filter));
            if (cores == null || cores.isEmpty()) {
                return Collections.emptyList();
            }
            List<Entity.Builder> builders = new ArrayList<>();
            Map<Long, Entity.Builder> id2builder = new HashMap<>();
            for (Map<String, Object> core : cores) {
                Entity.Builder builder = new Entity.Builder().
                        setCode((String)core.get("code")).
                        setName((String)core.get("name")).
                        setType(EntityType.valueOf((String)core.get("type"))).
                        setUtcCreatedTimestamp((Date)core.get("utc_created_timestamp"));
                for (String label : joinsData) {
                    String captionLabel = label.replaceFirst("\\.", "_");
                    builder.addJoin(label, core.get(captionLabel));
                }
                builders.add(builder);
                id2builder.put((Long)core.get("id"), builder);
            }
            // fetch profiles
            List<Map<String, Object>> profiles = mapper.selectProfiles(id2builder.keySet());
            for (Map<String, Object> prof : profiles) {
                id2builder.get((Long)prof.get("entity_id")).addProfile((String)prof.get("profile_key"), (String)prof.get("profile_value"));
            }
            // build results
            List<Entity> res = new ArrayList<>();
            for (Entity.Builder builder : builders) {
                res.add(builder.build());
            }
            return res;
        } finally {
            session.close();
        }
    }

    @Override
    public long countEntities(Set<Criterium<String>> criteria) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            // extract tables from criteria
            Set<String> tables = new HashSet<>();
            for (Criterium<String> crit : criteria) {
                String column = crit.getColumn();
                if (column.startsWith("join-")) {
                    tables.add(column.substring(5).split("\\.")[0]);
                }
            }
            // transform criteria
            Set<Criterium<String>> transformedCriteria = new HashSet<>();
            for (Criterium<String> crit : criteria) {
                String column = crit.getColumn();
                if (column.startsWith("core-")) {
                    column = column.substring(5);
                    if (column.equals("type")) {
                        column = "b.name";
                    }
                    else {
                        column = "a." + column;
                    }
                }
                if (column.startsWith("join-")) {
                    column = column.substring(5);
                    
                }
                transformedCriteria.add(Criterium.create(column, crit.getOperator(), crit.getValue()));
            }

            // 
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            return mapper.countCores(tables, transformedCriteria);
        } finally {
            session.close();
        }
    }

    @Override
    public void updateEntityName(String code, String name) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.updateName(code, name);
        } finally {
            session.close();
        }
    }
    
    @Override
    public void replaceEntityProfile(String code, Map<String, String> profile) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            Entity validationEntity = new Entity.Builder().
                    setCode("VE0000000000000000").
                    setName("Vaildation entity").
                    setType(EntityType.USER).
                    setUtcCreatedTimestamp(Dates.createDate(2000, Month.JANUARY, 1)).
                    setProfile(profile).
                    build();
            
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.deleteProfile(code);
            if (!validationEntity.getProfile().isEmpty()) {
                mapper.insertProfile(code, validationEntity.getProfile());
            }
        } finally {
            session.close();
        }
    }
    
    @Override
    public void updateEntityJoin(String code, String join, Object value) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            new Entity.Builder().
                    setCode("VE0000000000000000").
                    setName("Vaildation entity").
                    setType(EntityType.USER).
                    setUtcCreatedTimestamp(Dates.createDate(2000, Month.JANUARY, 1)).
                    addJoin(join, value).
                    build();
            String[] parts = join.split("\\.", 2);
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.updateJoin(code, parts[0], parts[1], value);
        } finally {
            session.close();
        }
    }

    @Override
    public void insertRelation(String subjectCode, String objectCode, RelationType type) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.insertRelation(subjectCode, objectCode, type);
        } finally {
            session.close();
        }
    }

    @Override
    public void deleteRelation(String subjectCode, String objectCode, RelationType type) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.deleteRelation(subjectCode, objectCode, type);
        } finally {
            session.close();
        }
    }

    @Override
    public List<Relation> selectRelations(Filter<RelationFilterColumns> filter) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            
            // transform filter
            Filter.Builder<String> transformedFilter = new Filter.Builder<>();
            transformedFilter.setFrom(filter.getFrom());
            transformedFilter.setLimit(filter.getLimit());
            for (Criterium<RelationFilterColumns> crit : filter.getCriteria()) {
                transformedFilter.addCriterium(new Criterium.Builder<String>().
                        setColumn(transformRelationColumnForCriterium(crit.getColumn())).
                        setOperator(crit.getOperator()).
                        setValue(crit.getValue()).
                        build());
            }
            for (Order<RelationFilterColumns> ord : filter.getOrders()) {
                transformedFilter.addOrder(new Order.Builder<String>().
                        setColumn(transformRelationColumnForOrder(ord.getColumn())).
                        setDirection(ord.getDirection()).
                        build());
            }
            List<Map<String, Object>> rels = mapper.selectRelations(transformedFilter.build());
            
            // build results
            List<Relation> res = new ArrayList<>();
            for (Map<String, Object> rel : rels) {
                res.add(new Relation.Builder().
                        setSubjectCode((String)rel.get("subject_code")).
                        setSubjectName((String)rel.get("subject_name")).
                        setSubjectType(EntityType.valueOf((String)rel.get("subject_type"))).
                        setObjectCode((String)rel.get("object_code")).
                        setObjectName((String)rel.get("object_name")).
                        setObjectType(EntityType.valueOf((String)rel.get("object_type"))).
                        setType(RelationType.valueOf((String)rel.get("type"))).
                        build());
            }
            return res;
        } finally {
            session.close();
        }
    }

    @Override
    public long countRelations(Set<Criterium<RelationFilterColumns>> criteria) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            // transform criteria
            Set<Criterium<String>> transformedCriteria = new HashSet<>();
            for (Criterium<RelationFilterColumns> crit : criteria) {
                transformedCriteria.add(Criterium.create(
                        transformRelationColumnForCriterium(crit.getColumn()), crit.getOperator(), crit.getValue()));
            }

            // 
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            return mapper.countRelations(transformedCriteria);
        } finally {
            session.close();
        }
    }

    @Override
    public void insertData(Data data, byte[] blob) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.insertData(data, blob);
        } finally {
            session.close();
        }
    }

    @Override
    public void deleteData(Data data) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            mapper.deleteData(data);
        } finally {
            session.close();
        }
    }

    @Override
    public List<Data> selectDatas(Filter<DataFilterColumns> filter) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            
            // transform filter
            Filter.Builder<String> transformedFilter = new Filter.Builder<>();
            transformedFilter.setFrom(filter.getFrom());
            transformedFilter.setLimit(filter.getLimit());
            for (Criterium<DataFilterColumns> crit : filter.getCriteria()) {
                transformedFilter.addCriterium(new Criterium.Builder<String>().
                        setColumn(transformDataColumnForCriterium(crit.getColumn())).
                        setOperator(crit.getOperator()).
                        setValue(crit.getValue()).
                        build());
            }
            for (Order<DataFilterColumns> ord : filter.getOrders()) {
                transformedFilter.addOrder(new Order.Builder<String>().
                        setColumn(transformDataColumnForOrder(ord.getColumn())).
                        setDirection(ord.getDirection()).
                        build());
            }
            List<Map<String, Object>> datas = mapper.selectDatas(transformedFilter.build());
            System.out.println(datas);

            // build results
            List<Data> res = new ArrayList<>();
            for (Map<String, Object> dat : datas) {
                res.add(new Data.Builder().
                        setEntityCode((String)dat.get("entity_code")).
                        setBinaryName((String)dat.get("binary_name")).
                        build());
            }
            return res;
        } finally {
            session.close();
        }
    }

    @Override
    public long countDatas(Set<Criterium<DataFilterColumns>> criteria) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            // transform criteria
            Set<Criterium<String>> transformedCriteria = new HashSet<>();
            for (Criterium<DataFilterColumns> crit : criteria) {
                transformedCriteria.add(Criterium.create(
                        transformDataColumnForCriterium(crit.getColumn()), crit.getOperator(), crit.getValue()));
            }

            // 
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            return mapper.countDatas(transformedCriteria);
        } finally {
            session.close();
        }
    }
    
    @Override
    public byte[] selectDataBlob(Data data) {
        SqlSession session = sqlSessionFactory.openSession();
        try {
            MyBatisEntityMapper mapper = session.getMapper(MyBatisEntityMapper.class);
            
            List<Map<String, Object>> res = mapper.selectDataBlob(data);
            return (byte[])res.get(0).get("binary_data");
        } finally {
            session.close();
        }
    }
    
    /**
     * Transform join labels into the table columns format.
     * Key of the map is table name. Values of the set are joins.
     *  
     * @param joins join labels to be transformed
     * @return transformed joins
     */
    private Map<String, Set<String>> transformJoinLabels(Set<String> joins) {
        Map<String, Set<String>> res = new HashMap<>();
        for (String join : joins) {
            String[] parts = join.split("\\.", 2);
            if (!res.containsKey(parts[0])) {
                res.put(parts[0], new HashSet<String>());
            }
            res.get(parts[0]).add(parts[1]);
        }
        return res;
    }
    
    /**
     * Transforms filter into table columns format.
     * 
     * @param filter filter to be transformed
     * @return column filter format
     */
    private Filter<String> transformFilter(Filter<String> filter) {
        Filter.Builder<String> res = new Filter.Builder<>();
        res.setFrom(filter.getFrom());
        res.setLimit(filter.getLimit());
        for (Criterium<String> crit : filter.getCriteria()) {
            String column = crit.getColumn();
            if (column.startsWith("core-")) {
                column = column.substring(5);
            }
            if (column.startsWith("join-")) {
                column = column.substring(5);
            }
            res.addCriterium(Criterium.create(column, crit.getOperator(), crit.getValue()));
        }
        for (Order<String> ord : filter.getOrders()) {
            String column = ord.getColumn();
            if (column.startsWith("core-")) {
                column = column.substring(5);
            }
            if (column.startsWith("join-")) {
                column = column.substring(5).replaceFirst("\\.", "_");
            }
            res.addOrder(Order.create(column, ord.getDirection()));
        }
        return res.build();
    }

    /**
     * Transform column for relation into dao format for criterium.
     * 
     * @param column column to be transformed
     * @return dao column
     */
    private String transformRelationColumnForCriterium(RelationFilterColumns column) {
        switch (column) {
        case SUBJECT_CODE:
            return "sc.code";
        case SUBJECT_NAME:
            return "sc.name";
        case SUBJECT_TYPE:
            return "st.name";
        case OBJECT_CODE:
            return "oc.code";
        case OBJECT_NAME:
            return "oc.name";
        case OBJECT_TYPE:
            return "ot.name";
        case TYPE:
            return "t.name";
        default:
            throw new IllegalArgumentException("unsupported relation column, please implement me: column = " + column);
        }
    }
    
    /**
     * Transform column for relation into dao format for ordering.
     * 
     * @param column column to be transformed
     * @return dao column
     */
    private String transformRelationColumnForOrder(RelationFilterColumns column) {
        switch (column) {
        case SUBJECT_CODE:
            return "subject_code";
        case SUBJECT_NAME:
            return "subject_name";
        case SUBJECT_TYPE:
            return "subject_type";
        case OBJECT_CODE:
            return "object_code";
        case OBJECT_NAME:
            return "object_name";
        case OBJECT_TYPE:
            return "object_type";
        case TYPE:
            return "type";
        default:
            throw new IllegalArgumentException("unsupported relation column, please implement me: column = " + column);
        }
    }

    /**
     * Transform column for data into dao format for criterium.
     * 
     * @param column column to be transformed
     * @return dao column
     */
    private String transformDataColumnForCriterium(DataFilterColumns column) {
        switch (column) {
        case ENTITY_CODE:
            return "e.code";
        case BINARY_NAME:
            return "ed.binary_name";
        default:
            throw new IllegalArgumentException("unsupported data column, please implement me: column = " + column);
        }
    }
    
    /**
     * Transform column for data into dao format for ordering.
     * 
     * @param column column to be transformed
     * @return dao column
     */
    private String transformDataColumnForOrder(DataFilterColumns column) {
        switch (column) {
        case ENTITY_CODE:
            return "entity_code";
        case BINARY_NAME: 
            return "binary_name";
        default:
            throw new IllegalArgumentException("unsupported data column, please implement me: column = " + column);
        }
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

}
